@boxpdf/html-writer 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +80 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +80 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -372,17 +372,20 @@ function number(value) {
|
|
|
372
372
|
// src/visual-font.ts
|
|
373
373
|
function visualFontAliases(pageNumber, fonts) {
|
|
374
374
|
return new Map(
|
|
375
|
-
fonts.filter(
|
|
375
|
+
fonts.filter(
|
|
376
|
+
(font) => (font.format === "truetype" || font.format === "opentype") && !/(?:courier|^TTE)/i.test(font.family ?? "")
|
|
377
|
+
).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
|
|
376
378
|
);
|
|
377
379
|
}
|
|
378
380
|
function visualFontFace(font, aliases) {
|
|
379
|
-
if (font.format !== "truetype") return "";
|
|
381
|
+
if (font.format !== "truetype" && font.format !== "opentype") return "";
|
|
380
382
|
const alias = aliases.get(font.id);
|
|
381
383
|
if (!alias) return "";
|
|
382
384
|
const styles2 = visualFontStyles(font.family, alias).filter(
|
|
383
385
|
(style) => !style.startsWith("font-family:")
|
|
384
386
|
);
|
|
385
|
-
|
|
387
|
+
const mime = font.format === "opentype" ? "font/otf" : "font/ttf";
|
|
388
|
+
return `@font-face{font-family:${alias};src:url(data:${mime};base64,${base64(font.data)}) format("${font.format}");${styles2.join(";")}}`;
|
|
386
389
|
}
|
|
387
390
|
function visualFontStyles(fontFamily, alias) {
|
|
388
391
|
const normalized = fontFamily?.toLowerCase() ?? "";
|
|
@@ -464,7 +467,9 @@ function vectorMedia(page, imageOptions) {
|
|
|
464
467
|
);
|
|
465
468
|
const aliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
466
469
|
const visualCodeFonts = new Set(
|
|
467
|
-
(page.fonts ?? []).filter(
|
|
470
|
+
(page.fonts ?? []).filter(
|
|
471
|
+
(font) => (font.format === "truetype" || font.format === "opentype") && font.visualCodeMapping
|
|
472
|
+
).map((font) => font.id)
|
|
468
473
|
);
|
|
469
474
|
return components.map((component, componentIndex) => {
|
|
470
475
|
const bounds2 = component.bounds;
|
|
@@ -1317,7 +1322,8 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
1317
1322
|
);
|
|
1318
1323
|
options.onSemanticStats?.(stats);
|
|
1319
1324
|
} else {
|
|
1320
|
-
|
|
1325
|
+
const documentFonts = { entries: [] };
|
|
1326
|
+
for await (const page of pages) await writePositionedPage(page, write, options, documentFonts);
|
|
1321
1327
|
}
|
|
1322
1328
|
await write("</main>");
|
|
1323
1329
|
if (includeDocument) await write("</body></html>");
|
|
@@ -1360,7 +1366,7 @@ async function pageToHtml(page, options = {}) {
|
|
|
1360
1366
|
);
|
|
1361
1367
|
return output;
|
|
1362
1368
|
}
|
|
1363
|
-
async function writePositionedPage(page, write, options) {
|
|
1369
|
+
async function writePositionedPage(page, write, options, documentFonts) {
|
|
1364
1370
|
const imageOptions = resolveImageOptions("visual", options);
|
|
1365
1371
|
const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
|
|
1366
1372
|
const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
|
|
@@ -1371,14 +1377,18 @@ async function writePositionedPage(page, write, options) {
|
|
|
1371
1377
|
await write(
|
|
1372
1378
|
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number3(displayWidth)}pt;height:${number3(displayHeight)}pt">`
|
|
1373
1379
|
);
|
|
1374
|
-
const
|
|
1380
|
+
const { aliases: fontAliases, fontsToEmit } = visualPageFonts(
|
|
1381
|
+
page.number,
|
|
1382
|
+
page.fonts ?? [],
|
|
1383
|
+
documentFonts
|
|
1384
|
+
);
|
|
1375
1385
|
const type3Fonts = new Map(
|
|
1376
1386
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
1377
1387
|
);
|
|
1378
1388
|
const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
|
|
1379
|
-
if ((options.includeStyles ?? true) &&
|
|
1389
|
+
if ((options.includeStyles ?? true) && fontsToEmit.length) {
|
|
1380
1390
|
await write(
|
|
1381
|
-
`<style>${
|
|
1391
|
+
`<style>${fontsToEmit.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
1382
1392
|
);
|
|
1383
1393
|
}
|
|
1384
1394
|
if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
|
|
@@ -1445,6 +1455,30 @@ async function writePositionedPage(page, write, options) {
|
|
|
1445
1455
|
}
|
|
1446
1456
|
await write("</div></section>");
|
|
1447
1457
|
}
|
|
1458
|
+
function visualPageFonts(pageNumber, fonts, documentFonts) {
|
|
1459
|
+
const aliases = visualFontAliases(pageNumber, fonts);
|
|
1460
|
+
if (!documentFonts) return { aliases, fontsToEmit: fonts };
|
|
1461
|
+
const fontsToEmit = [];
|
|
1462
|
+
for (const font of fonts) {
|
|
1463
|
+
if (!aliases.has(font.id) || font.format === "type3") continue;
|
|
1464
|
+
const existing = documentFonts.entries.find(
|
|
1465
|
+
(entry) => entry.format === font.format && equalBytes(entry.data, font.data)
|
|
1466
|
+
);
|
|
1467
|
+
if (existing) {
|
|
1468
|
+
aliases.set(font.id, existing.alias);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const alias = `boxpdf-document-font-${documentFonts.entries.length + 1}`;
|
|
1472
|
+
aliases.set(font.id, alias);
|
|
1473
|
+
documentFonts.entries.push({ alias, data: font.data, format: font.format });
|
|
1474
|
+
fontsToEmit.push(font);
|
|
1475
|
+
}
|
|
1476
|
+
return { aliases, fontsToEmit };
|
|
1477
|
+
}
|
|
1478
|
+
function equalBytes(left, right) {
|
|
1479
|
+
if (left.length !== right.length) return false;
|
|
1480
|
+
return left.every((value, index) => value === right[index]);
|
|
1481
|
+
}
|
|
1448
1482
|
function coalesceVisualSpans(spans) {
|
|
1449
1483
|
const output = [];
|
|
1450
1484
|
for (const span of spans) {
|
|
@@ -1470,11 +1504,14 @@ function canCoalesceVisualSpans(left, right) {
|
|
|
1470
1504
|
if (left.direction !== "ltr" || right.direction !== "ltr") return false;
|
|
1471
1505
|
if (/guardian/i.test(left.fontFamily ?? "")) return false;
|
|
1472
1506
|
if (left.glyphCodes || right.glyphCodes) return false;
|
|
1507
|
+
if (left.fontAssetId || right.fontAssetId) return false;
|
|
1473
1508
|
if (!sameVisualTextState(left, right)) return false;
|
|
1474
1509
|
const tolerance = Math.max(0.02, left.fontSize * 0.015);
|
|
1475
1510
|
if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
|
|
1476
1511
|
const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
|
|
1477
|
-
|
|
1512
|
+
if (right.hasLeadingSpace) return false;
|
|
1513
|
+
if (gap >= -tolerance && gap <= tolerance) return true;
|
|
1514
|
+
return right.textAdjustmentBefore !== void 0 && right.textAdjustmentBefore < 0 && Math.abs(right.textAdjustmentBefore - gap) <= 1e-3;
|
|
1478
1515
|
}
|
|
1479
1516
|
function sameVisualTextState(left, right) {
|
|
1480
1517
|
return Math.abs(left.fontSize - right.fontSize) <= 1e-3 && left.fontName === right.fontName && left.fontFamily === right.fontFamily && left.fontAssetId === right.fontAssetId && left.color === right.color && left.fillOpacity === right.fillOpacity && left.strokeColor === right.strokeColor && left.strokeWidth === right.strokeWidth && left.strokeOpacity === right.strokeOpacity && left.renderingMode === right.renderingMode && sameTransform(left.transform, right.transform);
|
|
@@ -1515,6 +1552,13 @@ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases
|
|
|
1515
1552
|
if (endIndex === startIndex) return void 0;
|
|
1516
1553
|
const baseline = pageHeight - first.bounds.y;
|
|
1517
1554
|
const lineSpans = spans.slice(startIndex, endIndex + 1);
|
|
1555
|
+
const dxRun = visualDxTextRun(lineSpans, fontAliases);
|
|
1556
|
+
if (dxRun) {
|
|
1557
|
+
return {
|
|
1558
|
+
html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}" dx="${dxRun.offsets.map(number3).join(" ")}">${escapeHtml4(dxRun.text)}</text>`,
|
|
1559
|
+
endIndex
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1518
1562
|
const content = lineSpans.map(
|
|
1519
1563
|
(span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
|
|
1520
1564
|
).join("");
|
|
@@ -1523,6 +1567,32 @@ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases
|
|
|
1523
1567
|
endIndex
|
|
1524
1568
|
};
|
|
1525
1569
|
}
|
|
1570
|
+
function visualDxTextRun(spans, fontAliases) {
|
|
1571
|
+
const first = spans[0];
|
|
1572
|
+
if (!first?.fontAssetId || !fontAliases.has(first.fontAssetId) || spans.length < 2)
|
|
1573
|
+
return void 0;
|
|
1574
|
+
let text = first.text;
|
|
1575
|
+
const offsets = characterOffsets(first, 0);
|
|
1576
|
+
for (let index = 1; index < spans.length; index += 1) {
|
|
1577
|
+
const previous = spans[index - 1];
|
|
1578
|
+
const current = spans[index];
|
|
1579
|
+
if (!previous || !current || current.fontAssetId !== first.fontAssetId) return void 0;
|
|
1580
|
+
const characters = [...current.text];
|
|
1581
|
+
if (characters.length === 0 || previous.naturalWidth === void 0) return void 0;
|
|
1582
|
+
const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
|
|
1583
|
+
const trailingSpacing = (previous.characterSpacing ?? 0) + (previous.text.endsWith(" ") ? previous.wordSpacing ?? 0 : 0);
|
|
1584
|
+
offsets.push(...characterOffsets(current, gap + trailingSpacing));
|
|
1585
|
+
text += current.text;
|
|
1586
|
+
}
|
|
1587
|
+
while (offsets.at(-1) === 0) offsets.pop();
|
|
1588
|
+
return offsets.length > 0 ? { text, offsets } : void 0;
|
|
1589
|
+
}
|
|
1590
|
+
function characterOffsets(span, first) {
|
|
1591
|
+
const characters = [...span.text];
|
|
1592
|
+
return characters.map(
|
|
1593
|
+
(_, index) => index === 0 ? first : (span.characterSpacing ?? 0) + (characters[index - 1] === " " ? span.wordSpacing ?? 0 : 0)
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1526
1596
|
function visualTextTspan(span, dx) {
|
|
1527
1597
|
const extent = span.bounds.width;
|
|
1528
1598
|
const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
|